package io.noties.markwon.base.core

import android.widget.TextView
import android.text.Spanned
import android.text.Spannable
import android.text.method.LinkMovementMethod
import androidx.annotation.VisibleForTesting
import io.noties.markwon.base.*
import io.noties.markwon.base.core.factory.*
import io.noties.markwon.base.core.spans.OrderedListItemSpan
import io.noties.markwon.base.core.spans.TextViewSpan
import io.noties.markwon.base.image.*
import org.commonmark.node.*

/**
 * @see io.noties.markwon.base.core.CoreProps
 *
 * @since 3.0.0
 */
class CorePlugin protected constructor() : AbstractMarkwonPlugin() {
    /**
     * @see .addOnTextAddedListener
     * @since 4.0.0
     */
    interface OnTextAddedListener {
        /**
         * Will be called when new text is added to resulting [SpannableBuilder].
         * Please note that only text represented by [Text] node will trigger this callback
         * (text inside code and code-blocks won\'t trigger it).
         *
         *
         * Please note that if you wish to add spans you must use `start` parameter
         * in order to place spans correctly (`start` represents the index at which `text`
         * was added). So, to set a span for the whole length of the text added one should use:
         *
         *
         * `visitor.builder().setSpan(new MySpan(), start, start + text.length(), 0);
        ` *
         *
         * @param visitor [MarkwonVisitor]
         * @param text    literal that had been added
         * @param start   index in `visitor` as which text had been added
         * @see .addOnTextAddedListener
         */
        fun onTextAdded(visitor: MarkwonVisitor, text: String, start: Int)
    }

    // @since 4.0.0
    private val onTextAddedListeners  = mutableListOf<OnTextAddedListener>()

    // @since 4.5.0
    private var hasExplicitMovementMethod = false

    /**
     * @since 4.5.0
     */
    fun hasExplicitMovementMethod(hasExplicitMovementMethod: Boolean): CorePlugin {
        this.hasExplicitMovementMethod = hasExplicitMovementMethod
        return this
    }

    /**
     * Can be useful to post-process text added. For example for auto-linking capabilities.
     *
     * @see OnTextAddedListener
     *
     * @since 4.0.0
     */
    fun addOnTextAddedListener(onTextAddedListener: OnTextAddedListener): CorePlugin {
        onTextAddedListeners.add(onTextAddedListener)
        return this
    }

    override fun configureVisitor(builder: MarkwonVisitor.Builder) {
        text(builder)
        strongEmphasis(builder)
        emphasis(builder)
        blockQuote(builder)
        code(builder)
        fencedCodeBlock(builder)
        indentedCodeBlock(builder)
        image(builder)
        bulletList(builder)
        orderedList(builder)
        listItem(builder)
        thematicBreak(builder)
        heading(builder)
        softLineBreak(builder)
        hardLineBreak(builder)
        paragraph(builder)
        link(builder)
    }

    override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {

        // reuse this one for both code-blocks (indent & fenced)
        val codeBlockSpanFactory = CodeBlockSpanFactory()
        builder
            .setFactory(StrongEmphasis::class.java, StrongEmphasisSpanFactory())
            .setFactory(Emphasis::class.java, EmphasisSpanFactory())
            .setFactory(BlockQuote::class.java, BlockQuoteSpanFactory())
            .setFactory(Code::class.java, CodeSpanFactory())
            .setFactory(FencedCodeBlock::class.java, codeBlockSpanFactory)
            .setFactory(IndentedCodeBlock::class.java, codeBlockSpanFactory)
            .setFactory(ListItem::class.java, ListItemSpanFactory())
            .setFactory(Heading::class.java, HeadingSpanFactory())
            .setFactory(Link::class.java, LinkSpanFactory())
            .setFactory(ThematicBreak::class.java, ThematicBreakSpanFactory())
    }

    override fun beforeSetText(textView: TextView, markdown: Spanned) {
        OrderedListItemSpan.measure(textView, markdown)

        // @since 4.4.0
        // we do not break API compatibility, instead we introduce the `instance of` check
        if (markdown is Spannable) {
            TextViewSpan.applyTo(markdown, textView)
        }
    }

    override fun afterSetText(textView: TextView) {
        // let's ensure that there is a movement method applied
        // we do it `afterSetText` so any user-defined movement method won't be
        // replaced (it should be done in `beforeSetText` or manually on a TextView)
        // @since 4.5.0 we additionally check if we should apply _implicit_ movement method
        if (!hasExplicitMovementMethod && textView.movementMethod == null) {
            textView.movementMethod = LinkMovementMethod.getInstance()
        }
    }

    private fun text(builder: MarkwonVisitor.Builder) {
        builder.on(Text::class.java) { visitor, text ->
            val literal = text.literal
            visitor.builder().append(literal)

            // @since 4.0.0
            if (onTextAddedListeners.isNotEmpty()) {
                // calculate the start position
                val length = visitor.length() - literal.length
                for (onTextAddedListener in onTextAddedListeners) {
                    onTextAddedListener.onTextAdded(visitor, literal, length)
                }
            }
        }
    }

    companion object {
        fun create(): CorePlugin {
            return CorePlugin()
        }

        /**
         * @return a set with enabled by default block types
         * @since 4.4.0
         */
        fun enabledBlockTypes(): Set<Class<out Block>> {
            return setOf(
                    BlockQuote::class.java,
                    Heading::class.java,
                    FencedCodeBlock::class.java,
                    HtmlBlock::class.java,
                    ThematicBreak::class.java,
                    ListBlock::class.java,
                    IndentedCodeBlock::class.java
                )
        }

        private fun strongEmphasis(builder: MarkwonVisitor.Builder) {
            builder.on(StrongEmphasis::class.java) { visitor, strongEmphasis ->
                val length = visitor.length()
                visitor.visitChildren(strongEmphasis)
                visitor.setSpansForNodeOptional(strongEmphasis, length)
            }
        }

        private fun emphasis(builder: MarkwonVisitor.Builder) {
            builder.on(Emphasis::class.java) { visitor, emphasis ->
                val length = visitor.length()
                visitor.visitChildren(emphasis)
                visitor.setSpansForNodeOptional(emphasis, length)
            }
        }

        private fun blockQuote(builder: MarkwonVisitor.Builder) {
            builder.on(BlockQuote::class.java) { visitor, blockQuote ->
                visitor.blockStart(blockQuote)
                val length = visitor.length()
                visitor.visitChildren(blockQuote)
                visitor.setSpansForNodeOptional(blockQuote, length)
                visitor.blockEnd(blockQuote)
            }
        }

        private fun code(builder: MarkwonVisitor.Builder) {
            builder.on(Code::class.java) { visitor, code ->
                val length = visitor.length()

                // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
                // unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted
                visitor.builder()
                    .append('\u00a0')
                    .append(code.literal)
                    .append('\u00a0')
                visitor.setSpansForNodeOptional(code, length)
            }
        }

        private fun fencedCodeBlock(builder: MarkwonVisitor.Builder) {
            builder.on(FencedCodeBlock::class.java) { visitor, fencedCodeBlock ->
                visitCodeBlock(
                    visitor,
                    fencedCodeBlock.info,
                    fencedCodeBlock.literal,
                    fencedCodeBlock
                )
            }
        }

        private fun indentedCodeBlock(builder: MarkwonVisitor.Builder) {
            builder.on(IndentedCodeBlock::class.java) { visitor, indentedCodeBlock ->
                visitSyntaxHighlightCodeBlock(
                    visitor,
                    null,
                    indentedCodeBlock.literal,
                    indentedCodeBlock
                )
            }
        }

        // @since 4.0.0
        // his method is moved from ImagesPlugin. Alternative implementations must set SpanFactory
        // for Image node in order for this visitor to function
        private fun image(builder: MarkwonVisitor.Builder) {
            builder.on(Image::class.java) { visitor, image -> // if there is no image spanFactory, ignore
                    val spanFactory = visitor.configuration().spansFactory()[Image::class.java]
                    if (spanFactory == null) {
                        visitor.visitChildren(image)
                        return@on
                    }
                    val length = visitor.length()
                    visitor.visitChildren(image)

                    // we must check if anything _was_ added, as we need at least one char to render
                    if (length == visitor.length()) {
                        visitor.builder().append('\uFFFC')
                    }
                    val configuration = visitor.configuration()
                    val parent = image.parent
                    val link = parent is Link
                    val destination = configuration
                        .imageDestinationProcessor()
                        .process(image.destination)
                    val props = visitor.renderProps()

                    // apply image properties
                    // Please note that we explicitly set IMAGE_SIZE to null as we do not clear
                    // properties after we applied span (we could though)
                    ImageProps.DESTINATION[props] = destination
                    ImageProps.REPLACEMENT_TEXT_IS_LINK[props] = link
                    ImageProps.IMAGE_SIZE[props] = null
                    visitor.setSpans(length, spanFactory.getSpans(configuration, props))
                }
        }

        @VisibleForTesting
        fun visitCodeBlock(
            visitor: MarkwonVisitor,
            info: String?,
            code: String,
            node: Node
        ) {
            visitSyntaxHighlightCodeBlock(visitor, info, code, node)
        }

        fun visitSyntaxHighlightCodeBlock(
            visitor: MarkwonVisitor,
            info: String?,
            code: String,
            node: Node
        ) {
            visitor.blockStart(node)
            val length = visitor.length()
            visitor.builder().append('\u00a0').append('\n')
                .append(visitor.configuration().syntaxHighlight().highlight(info, code))
            visitor.ensureNewLine()
            visitor.builder().append('\u00a0')

            // @since 4.1.1
            CoreProps.CODE_BLOCK_INFO[visitor.renderProps()] = info
            visitor.setSpansForNodeOptional(node, length)
            visitor.blockEnd(node)
        }


        private fun bulletList(builder: MarkwonVisitor.Builder) {
            builder.on(BulletList::class.java, SimpleBlockNodeVisitor())
        }

        private fun orderedList(builder: MarkwonVisitor.Builder) {
            builder.on(OrderedList::class.java, SimpleBlockNodeVisitor())
        }

        private fun listItem(builder: MarkwonVisitor.Builder) {
            builder.on(ListItem::class.java) { visitor, listItem ->
                val length = visitor.length()

                // it's important to visit children before applying render props (
                // we can have nested children, who are list items also, thus they will
                // override out props (if we set them before visiting children)
                visitor.visitChildren(listItem)
                val parent: Node = listItem.parent
                if (parent is OrderedList) {
                    val start = parent.startNumber
                    CoreProps.LIST_ITEM_TYPE[visitor.renderProps()] = CoreProps.ListItemType.ORDERED
                    CoreProps.ORDERED_LIST_ITEM_NUMBER[visitor.renderProps()] = start

                    // after we have visited the children increment start number
                    val orderedList = parent
                    orderedList.startNumber = orderedList.startNumber + 1
                } else {
                    CoreProps.LIST_ITEM_TYPE[visitor.renderProps()] = CoreProps.ListItemType.BULLET
                    CoreProps.BULLET_LIST_ITEM_LEVEL[visitor.renderProps()] = listLevel(listItem)
                }
                visitor.setSpansForNodeOptional(listItem, length)
                if (visitor.hasNext(listItem)) {
                    visitor.ensureNewLine()
                }
            }
        }

        private fun listLevel(node: Node): Int {
            var level = 0
            var parent = node.parent
            while (parent != null) {
                if (parent is ListItem) {
                    level += 1
                }
                parent = parent.parent
            }
            return level
        }

        private fun thematicBreak(builder: MarkwonVisitor.Builder) {
            builder.on(ThematicBreak::class.java) { visitor, thematicBreak ->
                visitor.blockStart(thematicBreak)
                val length = visitor.length()

                // without space it won't render
                visitor.builder().append('\u00a0')
                visitor.setSpansForNodeOptional(thematicBreak, length)
                visitor.blockEnd(thematicBreak)
            }
        }

        private fun heading(builder: MarkwonVisitor.Builder) {
            builder.on(Heading::class.java) { visitor, heading ->
                visitor.blockStart(heading)
                val length = visitor.length()
                visitor.visitChildren(heading)
                CoreProps.HEADING_LEVEL[visitor.renderProps()] = heading.level
                visitor.setSpansForNodeOptional(heading, length)
                visitor.blockEnd(heading)
            }
        }

        private fun softLineBreak(builder: MarkwonVisitor.Builder) {
            builder.on(SoftLineBreak::class.java) { visitor, softLineBreak ->
                visitor.builder().append(' ')
            }
        }

        private fun hardLineBreak(builder: MarkwonVisitor.Builder) {
            builder.on(HardLineBreak::class.java) { visitor, hardLineBreak -> visitor.ensureNewLine() }
        }

        private fun paragraph(builder: MarkwonVisitor.Builder) {
            builder.on(Paragraph::class.java) { visitor, paragraph ->
                val inTightList = isInTightList(paragraph)
                if (!inTightList) {
                    visitor.blockStart(paragraph)
                }
                val length = visitor.length()
                visitor.visitChildren(paragraph)
                CoreProps.PARAGRAPH_IS_IN_TIGHT_LIST[visitor.renderProps()] = inTightList

                // @since 1.1.1 apply paragraph span
                visitor.setSpansForNodeOptional(paragraph, length)
                if (!inTightList) {
                    visitor.blockEnd(paragraph)
                }
            }
        }

        private fun isInTightList(paragraph: Paragraph): Boolean {
            val parent: Node? = paragraph.parent
            if (parent != null) {
                val gramps = parent.parent
                if (gramps is ListBlock) {
                    return gramps.isTight
                }
            }
            return false
        }

        private fun link(builder: MarkwonVisitor.Builder) {
            builder.on(Link::class.java) { visitor, link ->
                val length = visitor.length()
                visitor.visitChildren(link)
                val destination = link.destination
                CoreProps.LINK_DESTINATION[visitor.renderProps()] = destination
                visitor.setSpansForNodeOptional(link, length)
            }
        }
    }
}